/*
 * Created on Mar 20, 2004
 * Created by Alon Rohter
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 */
package connect.peer;

import controller.config.COConfigurationManager;
import controller.config.ParameterListener;
import java.net.InetAddress;
import java.util.*;

import connect.peer.PEPeer;
import torrentlib.AENetworkClassifier;
import torrentlib.CRC32C;
import torrentlib.Constants;
import torrentlib.Debug;
import torrentlib.HostNameToIPResolver;
import torrentlib.SystemTime;
import plugins.peers.Peer;
import plugins.utils.LocationProvider;
import pluginsimpl.local.PluginCoreUtils;

import torrentlib.AzureusCoreFactory;
import controller.networkmanager.admin.NetworkAdmin;


/**
 * Varies peer connection utility methods.
 */
public class PeerUtils {

   private static final String	CONFIG_MAX_CONN_PER_TORRENT	= "Max.Peer.Connections.Per.Torrent";
   private static final String	CONFIG_MAX_CONN_TOTAL		= "Max.Peer.Connections.Total";
   
   public static int MAX_CONNECTIONS_PER_TORRENT;
   public static int MAX_CONNECTIONS_TOTAL;

   static{
   	
   	COConfigurationManager.addParameterListener(
   		CONFIG_MAX_CONN_PER_TORRENT,
   		new ParameterListener()
		{
   			public void 
			parameterChanged(
				String parameterName )
   			{
         MAX_CONNECTIONS_PER_TORRENT = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_PER_TORRENT);
   			}
		});
   	
   	MAX_CONNECTIONS_PER_TORRENT = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_PER_TORRENT);
   	
  	COConfigurationManager.addParameterListener(
  			CONFIG_MAX_CONN_TOTAL,
  	   		new ParameterListener()
  			{
  	   			public void 
  				parameterChanged(
  					String parameterName )
  	   			{
             MAX_CONNECTIONS_TOTAL = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_TOTAL);
  	   			}
  			});
  	   	
  	MAX_CONNECTIONS_TOTAL = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_TOTAL);
   }
   
   private static final NetworkAdmin	network_admin;
   
   private static volatile long		na_last_ip4_time;
   private static volatile long		na_last_ip6_time;
   private static volatile byte[]	na_last_ip4;
   private static volatile byte[]	na_last_ip6;

   private static int	na_tcp_port;
   
   static{
	   NetworkAdmin temp = null;
	   
	   try{
		   temp = NetworkAdmin.getSingleton();
		   
	   }catch( Throwable e ){
	   }
	   
	   network_admin = temp;
	   
	   COConfigurationManager.addAndFireParameterListener(
			 "TCP.Listen.Port",
			 new ParameterListener() 
			 {	
				public void 
				parameterChanged(
					String parameterName ) 
				{
					na_tcp_port = COConfigurationManager.getIntParameter( parameterName );
				}
			});
   }
   
   public static int
   getPeerPriority(
	   String 	address,
	   int		port )
   {
	   if ( network_admin == null ){
		   
		   return( 0 );
	   }
	   
	   try{
	   
		   InetAddress ia = HostNameToIPResolver.syncResolve( address );
		   
		   if ( ia != null ){
			   
			   return( getPeerPriority( ia, port ));
		   }
		   
	   }catch( Throwable e ){
	   }
	   
	   return( 0 );
   }
   

   
   public static int
   getPeerPriority(
		InetAddress		address,
		int				peer_port )
   { 
	  return( getPeerPriority( address.getAddress(), peer_port ));
   }
   
   public static int
   getPeerPriority(
		byte[]		peer_address,
		short		peer_port )
   {
	   return( getPeerPriority( peer_address, ((int)peer_port)&0xffff ));
   }
   
   public static int
   getPeerPriority(
		byte[]		peer_address,
		int			peer_port )
   {
	   if ( network_admin == null ){
		   
		   return( 0 );
	   }

	   if ( peer_address == null ){
		   
		   return( 0 );
	   }
	   
	   byte[] my_address = null;
	   
	   long now = SystemTime.getMonotonousTime();
	   
	   if ( peer_address.length == 4 ){
		   
		   if ( na_last_ip4 != null && now - na_last_ip4_time < 120*1000 ){
			   
			   my_address = na_last_ip4;
			   
		   }else{
			  
			   if ( na_last_ip4_time == 0 || now - na_last_ip4_time > 10*1000 ){
			   
				   na_last_ip4_time = now;
				   
				   InetAddress ia = network_admin.getDefaultPublicAddress( true );
				   
				   if ( ia != null ){
					   
					   byte[] iab = ia.getAddress();
					   
					   if ( iab != null ){
						   
						   na_last_ip4 = my_address = ia.getAddress();
					   }
				   }
			   }
		   }  
		   
		   if ( my_address == null ){
			   
			   my_address = na_last_ip4;
		   }
	   }else if ( peer_address.length == 16 ){
		   
		   if ( na_last_ip6 != null && now - na_last_ip6_time < 120*1000 ){
			   
			   my_address = na_last_ip6;
			   
		   }else{
			  
			   if ( na_last_ip6_time == 0 || now - na_last_ip6_time > 10*1000 ){
			   
				   na_last_ip6_time = now;
				   
				   InetAddress ia = network_admin.getDefaultPublicAddressV6( true );
				   
				   if ( ia != null ){
					   
					   byte[] iab = ia.getAddress();
					   
					   if ( iab != null ){
						   
						   na_last_ip6 = my_address = ia.getAddress();
					   }
				   }
			   }
		   } 
		   
		   if ( my_address == null ){
			   
			   my_address = na_last_ip6;
		   }
	   }else{
		   
		   return( 0 );
	   }
	   
	   if ( my_address != null && my_address.length == peer_address.length ){
		   
		   return( getPeerPriority( my_address, na_tcp_port, peer_address, peer_port ));
		   
	   }else{
		   
		   return( 0 );
	   }
   }
   
   private static int
   getPeerPriority(
		 byte[]		a1,
		 int		port1,
		 byte[]		a2,
		 int		port2 )
   {
	   		// http://www.bittorrent.org/beps/bep_0040.html
	   	   
	   byte[] a1_masked = new byte[a1.length];
	   byte[] a2_masked = new byte[a2.length];
	   	   
	   /*
	   The formula to be used in prioritizing peers is this:

	   priority = crc32-c(sort(masked_client_ip, masked_peer_ip))
	   If the IP addresses are the same, the port numbers (16-bit integers) should be used instead:

	   priority = crc32-c(sort(client_port, peer_port))
	   For an IPv4 address, the mask to be used should be FF.FF.55.55 unless the IP addresses are in the same /16. 
	   In that case, the mask to be used should be FF.FF.FF.55. If the IP addresses are in the same /24, the entire address should be used (mask FF.FF.FF.FF).

	   For an IPv6 address, the mask should be derived in the same way, beginning with FFFF:FFFF:FFFF:5555:5555:5555:5555:5555. 
	   If the IP addresses are in the same /48, the mask to be used should be FFFF:FFFF:FFFF:FF55:5555:5555:5555:5555. 
	   If the IP addresses are in the same /56, the mask to be used should be FFFF:FFFF:FFFF:FFFF:5555:5555:5555:5555, etc...
	   */
	   
	   int x = a1_masked.length==4?1:5;
	   
	   boolean	same = true;

	   int order = 0;
	   
	   for ( int i=0;i<a1_masked.length;i++){
		   byte	a1_byte = a1[i];
		   byte	a2_byte = a2[i];

		   if ( i < x || same ){
			   a1_masked[i] = a1_byte;
			   a2_masked[i] = a2_byte;
		   }else{
			   a1_masked[i] = (byte)(a1_byte&0x55);
			   a2_masked[i] = (byte)(a2_byte&0x55);
		   }
		   
		   if ( i >= x && same ){
			   same = a1_byte == a2_byte;
		   }
		   
		   if ( order == 0 ){
			  			   
			   order = (a1_masked[i]&0xff) - (a2_masked[i]&0xff);
		   }
	   }
	   
	   if ( same ){
		   
		   a1_masked = new byte[]{ (byte)(port1>>8), (byte)port1 };
		   a2_masked = new byte[]{ (byte)(port2>>8), (byte)port2 };
		   
		   order = port1 - port2;
	   }
	   
	   CRC32C crc32 = new CRC32C();
	   
	   if ( order < 0 ){
		   
		   crc32.updateWord( a1_masked, true );
		   crc32.updateWord( a2_masked, true );
		   
	   }else{
		   
		   crc32.updateWord( a2_masked, true );
		   crc32.updateWord( a1_masked, true );
	   }
	   
	   long res = crc32.getValue();
	   
	   return( (int)res );
   }
   
  /**
   * Get the number of new peer connections allowed for the given data item,
   * within the configured per-torrent and global connection limits.
   * @return max number of new connections allowed, or -1 if there is no limit
   */
  public static int 
  numNewConnectionsAllowed( 
	 PeerIdentityDataID 	data_id, 
	 int 					specific_max )
  {
	 // max will have been adjusted based on network selection so we don't actually need to use
	 // nework info any further during this calculation
	  
    int curConnPerTorrent = PeerIdentityManager.getIdentityCount( data_id );
	  
    int curConnTotal = PeerIdentityManager.getTotalIdentityCount();
	    
    	// specific max here will default to the global per-torrent default if not explicitly set
    	// so we don't need to consider CONFIG_MAX_CONN_PER_TORRENT seperately
    
    int	PER_TORRENT_LIMIT = specific_max;
    
    int perTorrentAllowed = -1;  //default unlimited
    if ( PER_TORRENT_LIMIT != 0 ) {  //if limited
      int allowed = PER_TORRENT_LIMIT - curConnPerTorrent;
      if ( allowed < 0 )  allowed = 0;
      perTorrentAllowed = allowed;
    }
	    
    int totalAllowed = -1;  //default unlimited
    if ( MAX_CONNECTIONS_TOTAL != 0 ) {  //if limited
      int allowed = MAX_CONNECTIONS_TOTAL - curConnTotal;
      if ( allowed < 0 )  allowed = 0;
      totalAllowed = allowed;
    }
	    
    int allowed = -1;  //default unlimited
    if ( perTorrentAllowed > -1 && totalAllowed > -1 ) {  //if both limited
      allowed = Math.min( perTorrentAllowed, totalAllowed );
    }
    else if ( perTorrentAllowed == -1 || totalAllowed == -1 ) {  //if either unlimited
      allowed = Math.max( perTorrentAllowed, totalAllowed );
    }
	    
    return allowed;
  }
  

	private static Set<Integer>	ignore_peer_ports	= new HashSet<Integer>();
	
	static{
		COConfigurationManager.addParameterListener(
				"Ignore.peer.ports",
				new ParameterListener()
				{
					public void 
					parameterChanged(
						String parameterName )
					{
						readIgnorePeerPorts();
					}
				});
		
		readIgnorePeerPorts();
	}
	
	private static void
	readIgnorePeerPorts()
	{
		// XXX Optimize me for ranges!!
		String	str = COConfigurationManager.getStringParameter( "Ignore.peer.ports" ).trim();
		
		ignore_peer_ports.clear();
		
		if ( str.length() > 0 ){
			
			String[] ports = str.split("\\;");
			if (ports != null && ports.length > 0) {
				for (int i = 0; i < ports.length; i++) {
					String port = ports[i];
					int spreadPos = port.indexOf('-');
					if (spreadPos > 0 && spreadPos < port.length() - 1) {
						try {
							int iMin = Integer.parseInt(port.substring(0, spreadPos).trim());
							int iMax = Integer.parseInt(port.substring(spreadPos + 1).trim());
							
							iMin = Math.max( 0, iMin );
							iMax = Math.min(65535, iMax );
							
							for (int j = iMin; j <= iMax; j++) {
								ignore_peer_ports.add(Integer.valueOf(j));
							}
						} catch (Throwable e) {
							Debug.out( "Invalid ignore-port entry: " + port );
						}
					} else {
						try{
							ignore_peer_ports.add(Integer.parseInt(port.trim()));
						} catch (Throwable e) {
							Debug.out( "Invalid ignore-port entry: " + port );
						}
					}
				}
			}
		}
	}
	
	public static boolean
	ignorePeerPort(
		int		port )
	{
		return( ignore_peer_ports.contains( port ));
	}
	
	static final String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

	public static byte[]
	createPeerID()
	{
		byte[] peerId = new byte[20];
	
		byte[] version = Constants.VERSION_ID;
    
		for (int i = 0; i < 8; i++) {
			peerId[i] = version[i];
		}
    
	 	for (int i = 8; i < 20; i++) {
		  int pos = (int) ( Math.random() * chars.length());
		  peerId[i] = (byte)chars.charAt(pos);
		}
	 	
		// System.out.println( "generated new peer id:" + ByteFormatter.nicePrint(peerId));

	 	return( peerId );
	}
	
	public static byte[]
	createWebSeedPeerID()
	{
		byte[] peerId = new byte[20];

		peerId[0] = '-';
		peerId[1] = 'W';
		peerId[2] = 'S';
		
		for (int i = 3; i < 20; i++) {
			int pos = (int) ( Math.random() * chars.length());
			peerId[i] = (byte)chars.charAt(pos);
		}

		// System.out.println( "generated new peer id:" + ByteFormatter.nicePrint(peerId));

		return( peerId );
	}
	
	
	private static volatile LocationProvider	country_provider;
	private static long							country_provider_last_check;

	private static Object	country_key 	= new Object();
	private static Object	net_key 		= new Object();
		
	private static LocationProvider
	getCountryProvider()
	{
		if ( country_provider != null ){
			
			if ( country_provider.isDestroyed()){
				
				country_provider 			= null;
				country_provider_last_check	= 0;
			}
		}
		
		if ( country_provider == null ){
			
			long	now = SystemTime.getMonotonousTime();
			
			if ( country_provider_last_check == 0 || now - country_provider_last_check > 20*1000 ){
				
				country_provider_last_check = now;
				
				java.util.List<LocationProvider> providers = AzureusCoreFactory.getSingleton().getPluginManager().getDefaultPluginInterface().getUtilities().getLocationProviders();
	
				for ( LocationProvider provider: providers ){
					
					if ( 	provider.hasCapabilities( 
								LocationProvider.CAP_ISO3166_BY_IP |
								LocationProvider.CAP_COUNTY_BY_IP )){
						
						country_provider = provider;
					}
				}
			}
		}
		
		return( country_provider );
	}
	
	public static String[]
 	getCountryDetails(
 		Peer	peer )
	{
		return( getCountryDetails( PluginCoreUtils.unwrap( peer )));
	}
	
	public static String[]
	getCountryDetails(
		PEPeer	peer )
	{
		if ( peer == null ){
			
			return( null );
		}
		
		String[] details = (String[])peer.getUserData( country_key );
		
		if ( details == null ){
			
			LocationProvider lp = getCountryProvider();
			
			if ( lp != null ){
				
				try{
					String ip = peer.getIp();
										
					if ( HostNameToIPResolver.isDNSName( ip )){
						
						InetAddress peer_address = HostNameToIPResolver.syncResolve( ip );
						
						String code = lp.getISO3166CodeForIP( peer_address );
						String name = lp.getCountryNameForIP( peer_address, Locale.getDefault());
						
						if ( code != null && name != null ){
							
							details = new String[]{ code, name };
							
						}else{
							
							details = new String[0];
						}
					}else{
						
						String cat =  AENetworkClassifier.categoriseAddress( ip );
						
						if ( cat != AENetworkClassifier.AT_PUBLIC ){
							
							details = new String[]{ cat, cat };
							
						}else{
							
							details = new String[0];
						}
					}
					
					peer.setUserData( country_key, details );
					
				}catch( Throwable e ){	
				}
			}
		}
		
		return( details );
	}
	
	public static String
	getNetwork(
		PEPeer	peer )
	{
		if ( peer == null ){
			
			return( null );
		}
		
		String net = (String)peer.getUserData( net_key );
		
		if ( net == null ){
			
			net = AENetworkClassifier.categoriseAddress( peer.getIp());
			
			peer.setUserData( net_key, net );
		}
		
		return( net );
	}

	/*
	public static void
	main(
		String[]	args )
	{
	
		// If the client is 123.213.32.10 and the peer is 98.76.54.32, the hash that they should both arrive at is crc32-c(624C14007BD50000) or BB97323E.
		// If the client is 123.213.32.10 and the peer is 123.213.32.234, the hash that they should both arrive at is crc32-c[(7BD5200A7BD520EA) or C816B840.
	
		
		CRC32C crc = new CRC32C();
		
		crc.update("The quick brown fox jumps over the lazy dog".getBytes());
		
		System.out.println( Long.toString( crc.getValue(), 16 ));
		
		try{
			if ( getPeerPriority( InetAddress.getByName( "123.213.32.10" ).getAddress(), 10,   InetAddress.getByName( "98.76.54.32" ).getAddress(), 10 ) != 0xBB97323E ){
				
				System.out.println( "derp1" );
			}
			if ( getPeerPriority( InetAddress.getByName( "123.213.32.10" ).getAddress(), 10,   InetAddress.getByName( "123.213.32.234" ).getAddress(), 10 ) != 0xC816B840 ){
				System.out.println( "derp2" );
			}
			if ( getPeerPriority( InetAddress.getByName( "123.213.32.10" ).getAddress(), 10,   InetAddress.getByName( "123.213.32.10" ).getAddress(), 20 ) != 1879809021 ){
				System.out.println( "derp3" );
			}
		}catch( Throwable e ){
			e.printStackTrace();
			
		}
	}
	*/
}
